package router;

import java.net.InetAddress;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;

import com.sun.xml.internal.fastinfoset.tools.PrintTable;

import managers.RoutersManager;

import server.UDPServer;

import client.UDPClient;

/**
 * 
 * Classe que representa um roteador.
 * 
 * @author Arinaldo
 * @author Ana Gabrielle
 * @author Romeryto
 * 
 */
public class Router {
	
	private String id;
	private InetAddress ipAddress;
	private int port;
	private RouteTable routeTable;
	//<Id, custo>
	private HashMap<String, Integer> neighbors = new HashMap<String, Integer>();
	private UDPClient client;
	private UDPServer server;
	private boolean isOn = false;
	private RoutersManager routerManager;
	
	/**
	 * Cria um roteador
	 * @param id O ID que identifica o roteador
	 * @param ipAddress O seu endere�o IP
	 * @param port A porta do seu socket
	 */
	public Router(String id, InetAddress ipAddress, int port)  {
		this.id = id;
		this.ipAddress = ipAddress;
		this.port = port;
	}

	/**
	 * Retorna o ID do roteador
	 * @return o ID do roteador
	 */
	public String getId() {
		return id;
	}

	/**
	 * Seta o ID do roteador
	 * @param id o ID do roteador
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * Retorna o endere�o IP do roteador
	 * @return o endere�o IP do roteador
	 */
	public InetAddress getIpAddress() {
		return ipAddress;
	}

	/**
	 * Seta o endere�o IP do roteador
	 * @param ipAddress o IP do roteador
	 */
	public void setIpAddress(InetAddress ipAddress) {
		this.ipAddress = ipAddress;
	}

	/**
	 * Retorna a porta do socket do roteador
	 * @return a porta do seu socket
	 */
	public int getPort() {
		return port;
	}

	/**
	 * Seta a porta do socket do roteador
	 * @param port a porta do seu socket
	 */
	public void setPort(int port) {
		this.port = port;
	}

	/**
	 * Retorna a tabela que cont�m as informa��es dos custos para alcan�ar todos os roteadores ligados � rede
	 * @return a tabela contendo os custos
	 */
	public RouteTable getRouteTable() {
		return routeTable;
	}

	/**
	 * Seta a tabela com as informa��es dos custos para alcan�ar todos os roteadores ligados � rede
	 * @param routeTable a tabela contendo os custos
	 */
	public void setRouteTable(RouteTable routeTable) {
		this.routeTable = routeTable;
	}
	
	/**
	 * Retorna os vizinhos do roteador, no formato <IDdoVizinho, CustoAteEle>
	 * @return os vizinhos do roteador
	 */
	public HashMap<String, Integer> getNeighbors() {
		return neighbors;
	}

	/**
	 * Seta os vizinhos do roteador
	 * @param neighbors um mapa contendo os vizinhos do roteador, organizados no formato <IDdoVizinho, CustoAteEle>
	 */
	public void setNeighbors(HashMap<String, Integer> neighbors) {
		this.neighbors = neighbors;
	}

	/**
	 * Retorna o cliente UDP
	 * @return o cliete UDP
	 */
	public UDPClient getClient() {
		return client;
	}

	/**
	 * Seta o cliente UDP
	 * @param client o cliente UDP
	 */
	public void setClient(UDPClient client) {
		this.client = client;
	}

	/**
	 * Retorna o servidor UDP 
	 * @return o servidor UDP
	 */
	public UDPServer getServer() {
		return server;
	}

	/**
	 * Seta o servidor UDP
	 * @param server o servido UDP
	 */
	public void setServer(UDPServer server) {
		this.server = server;
	}
	
	/**
	 * Verifica se este roteador est� ligado
	 * @return true, caso esteja ligado; false, caso contr�rio
	 */
	public boolean isOn() {
		return this.isOn;
	}
	
	/**
	 * Seta se o roteador est� ligado ou desligado
	 * @param isOn true, caso deseje ligar o roteador; false, caso contr�rio
	 */
	public void setOn(boolean isOn) {
		this.isOn = isOn;
	}

	/**
	 * Seta o manager dos roteadores
	 * @param routerManager o gerenciador dos roteadores
	 */
	public void setRouterManager(RoutersManager routerManager) {
		this.routerManager = routerManager;
	}
	
	/**
	 * Imprime as informa��es do roteador - seu ID, seu endere�o de IP, a porta do seu socket; 
	 * seguidos da tabela com os custos
	 */
	public String toString() {
		return this.id + " " + this.ipAddress.toString() + " " + Integer.toString(this.port) + "\n" + routeTable.toString();
	}
	
	/**
	 * Imprime os IDs dos vizinhos
	 *
	 */
	@SuppressWarnings("unchecked")
	public void printNeighbors() {
		Set keySet = neighbors.keySet();
		Iterator iterator = keySet.iterator();
		System.out.println("Vizinhos do roteador");
		while(iterator.hasNext()) {
			System.out.println(iterator.next().toString());
		}
	}

	/**
	 * Seta a tabela com as informa��es dos custos para alcan�ar todos os roteadores ligados � rede
	 * @param routeTable a tabela contendo os custos
	 */
	public void setTable(RouteTable routerTable) {
		this.routeTable = routerTable;
		
	}

	/**
	 * Comunica aos vizinhos que este roteador foi ligado
	 * @param neighbors os vizinhos que devem ser avisados
	 */
	@SuppressWarnings("unchecked")
	public void pingNeighbors(HashMap<String, Integer> neighbors) {
		Set keySet = neighbors.keySet();
		Iterator iterator = keySet.iterator();
		while(iterator.hasNext()) {
			Router tempRouter = routerManager.getRouterById(iterator.next().toString());
			UDPClient udpClient = new UDPClient(tempRouter.getIpAddress(), tempRouter.getPort());
			udpClient.setRouter(this);
			udpClient.start();
		}
		
	}

	/**
	 * Verifica se precisa atualizar sua tabela, ao receber a tabela de um vizinho que foi modificada
	 * @param neighborID o ID do vizinho que teve sua tabela modificada
	 * @param neighborTableAsString a nova tabela do vizinho
	 */
	public void updateTable(String neighborID, String neighborTableAsString) {
		boolean updated = bellmanFord(neighborID, neighborTableAsString);
	}

    
    /**
     * Executa o algoritmo do Bellman Ford distribuido
     * @param neighborId o ID do roteador vizinho que foi recentemente atualizado
     * @param neighborTableAsString a nova tabela do vizinho
     * @return true, caso as modificacoes do vizinho tenham causado alguma alteracao na tabela do
     * roteador atual; false, caso contrario.
     */
    private boolean bellmanFord(String neighborId, String neighborTableAsString/*, Router router*/){
        boolean wasUpdated = false;

        int actualCostToNeighbor = this.routeTable.getDistanceTo(neighborId);
        int newCostToNeighbor = this.routerManager.getCost(neighborId, this.id);
        //atualiza a minha table com a custo do meu vizinho ate mim
        if (actualCostToNeighbor > newCostToNeighbor){
        		this.routeTable.setDistanceTo(neighborId, newCostToNeighbor);
        		this.routeTable.updateRoute(neighborId, neighborId);
        		System.out.println(this.toString());
                wasUpdated = true;
        }

        actualCostToNeighbor = this.routeTable.getDistanceTo(neighborId);
        StringTokenizer table = new StringTokenizer(neighborTableAsString);
        
        //verifica se a minha distancia ate todos os roteadores é realmente a menor, calculando atraves da tabela do vizinho 
        for (int i = 0; i < routerManager.getSize(); i++) {
        	int costToI = Integer.parseInt(table.nextToken(" "));
        	
        	if(i+1 != Integer.parseInt(neighborId)) {//nao atualiza mais a distancia para o vizinho
        		int actualCostToI = this.routeTable.getDistanceTo(Integer.toString(i+1));
        		int newCostToI = costToI + actualCostToNeighbor;
        		
        		if(newCostToI < actualCostToI) { 
        			this.routeTable.setDistanceTo(Integer.toString(i+1), newCostToI);
        			this.routeTable.updateRoute(Integer.toString(i+1), neighborId);
        			System.out.println(this.toString());
        		}
        		wasUpdated = true;
        	}

        }
        
        
            return wasUpdated;
            
    }
	
    /**
     * Liga um vizinho
     * @param port a porta do seu socket
     */
	public void setNeighborOn(int port) {
		this.routerManager.setRouterOn(port);
		
	}

	/**
	 * Desliga um vizinho
	 * @param serverPort a porta do seu socket
	 */
	public void setNeighborOff(int serverPort) {
		this.routerManager.setRouterOff(port);
		
	}

}
